package com.homestead.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.homestead.constant.RedisCacheKeyConstant;
import com.homestead.constant.RedisLockKeyConstant;
import com.homestead.domain.dos.CouponThemeCache;
import com.homestead.domain.entity.CouponTheme;
import com.homestead.domain.mapper.CouponThemeMapper;
import com.homestead.exception.BusinessException;
import com.homestead.exception.CouponThemeErrorCode;
import com.homestead.service.CouponThemeCacheService;
import com.homestead.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.redisson.api.RBucket;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.validation.constraints.NotNull;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author yhb
 * @date 2021-06-10 8:36
 */
@Service
@Slf4j
public class CouponThemeCacheServiceImpl extends ServiceImpl<CouponThemeMapper, CouponTheme> implements CouponThemeCacheService {
    @Autowired
    private RedissonClient redissonClient;
    /**
     * 获取coupon_theme表的缓存
     * @param couponThemeId 券活动主键id
     * @return 券活动缓存bean all字段
     */
    @Override
    public CouponThemeCache getById(Long couponThemeId) {
        return getFullFields(couponThemeId);
    }

    /**
     * 获取coupon_theme表指定字段的缓存
     * @param couponThemeId 券活动主键id
     * @param selectedFields 需要获取的字段set
     * @return couponThemeCache缓存数据 选中字段
     */
    @Override
    public CouponThemeCache getSelectedFieldsById(Long couponThemeId, Set<CouponThemeCache.FIELDS> selectedFields) {
        return doGetSelectedFieldsFromCouponThemeCache(couponThemeId, selectedFields);
    }

    /**
     * 添加缓存
     * @param couponThemeCache 券活动缓存bean
     */
    @Override
    public void add(@NotNull CouponThemeCache couponThemeCache) {
        validateCouponThemeCacheBean(couponThemeCache);
        RMap<String, Object> rMap = getCouponThemeRMap(couponThemeCache.getId());
        doAddCouponThemeCache(couponThemeCache, rMap);
    }

    /**
     * 更新指定字段属性值到couponThemeCache中
     * @param couponThemeCache 不为null的字段将update入缓存
     * @return true/false
     */
    @Override
    public boolean updateById(CouponThemeCache couponThemeCache) {
        if (couponThemeIdIsNull(couponThemeCache)) {
            throw new BusinessException(CouponThemeErrorCode.COUPON_THEME_ID_IS_NULL);
        }
        Map<String, Object> selectedFilesMap = BeanUtil.beanToMap(couponThemeCache, false, true);
        selectedFilesMap.remove(CouponThemeCache.FIELDS.ID.getName());
        return updateSelectedFields(couponThemeCache.getId(), selectedFilesMap);
    }

    private boolean updateSelectedFields(Long couponThemeId, Map<String, Object> selectedFilesMap) {
        // 校验字段
        for (String field : selectedFilesMap.keySet()) {
            CouponThemeCache.FIELDS.of(field);
        }
        // 获取redis 连接对象
        RMap<String, Object> rMap = getCouponThemeRMap(couponThemeId);
        if (!rMap.isExists()) {
            String lockKeyName = RedisLockKeyConstant.SYNC_COUPON_THEME_DATABASE_TO_CACHE + couponThemeId;
            return RedisUtil.executeLock(lockKeyName, 60, TimeUnit.SECONDS, () -> {
                if (!rMap.isExists()) {
                    CouponThemeCache couponThemeCache = rebuildCouponThemeCache(couponThemeId, rMap);
                    if (couponThemeCache == null) {
                        return false;
                    }
                    setSelectedFieldsToCouponThemeCache(selectedFilesMap, rMap);
                    return true;
                }
                setSelectedFieldsToCouponThemeCache(selectedFilesMap, rMap);
                return true;
            });
        }
        setSelectedFieldsToCouponThemeCache(selectedFilesMap, rMap);
        return true;
    }

    /**
     * 判断couponThemeId是否被标记为不存在
     * @param couponThemeId 券活动主键id
     * @return true:被标记了 false: 未被标记
     */
    @Override
    public boolean couponThemeIdIsMarkedAsNotExist(Long couponThemeId) {
        // 检查bloomFilter
        /*if (couponThemeBloomFilter.isExists() && couponThemeBloomFilter.contains(couponThemeId)) {
            log.error("bloomFilter filterFlag=true, couponThemeId={}", couponThemeId);
            return true;
        }*/
        String cacheKey = RedisCacheKeyConstant.NON_EXIST_COUPON_THEME_ID + couponThemeId;
        RBucket<Integer> rBucket = redissonClient.getBucket(cacheKey);
        boolean flag = rBucket.isExists();
        if (flag) {
            log.error("couponThemeIdIsNonExist: couponThemeId={}，在redis中被标记为不存在", couponThemeId);
        }
        return flag;
    }

    private boolean couponThemeIdIsNull(CouponThemeCache couponThemeCache) {
        return couponThemeCache.getId() == null;
    }

    private void validateCouponThemeCacheBean(CouponThemeCache couponThemeCache) {
        Class<?> clazz = couponThemeCache.getClass();
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field : declaredFields) {
            try {
                field.setAccessible(true);
                Object o = field.get(couponThemeCache);
                if (Objects.isNull(o)) {
                    throw new IllegalArgumentException("add couponThemeCache 缓存error: 字段["+ field.getName() + "]不能为空！couponThemeCache=" + JSON.toJSONString(couponThemeCache));
                }
            } catch (IllegalAccessException e) {
                log.error("add couponThemeCache error:", e);
                throw new RuntimeException("add couponThemeCache error", e);
            }

        }
    }

    private CouponThemeCache getFullFields(Long couponThemeId) {
        Set<CouponThemeCache.FIELDS> allFields = Stream
                .of(CouponThemeCache.FIELDS.values())
                .collect(Collectors.toSet());

        return doGetSelectedFieldsFromCouponThemeCache(couponThemeId, allFields);
    }

    private void setSelectedFieldsToCouponThemeCache(Map<String, Object> selectedFieldsMap, RMap<String, Object> rMap) {
        rMap.putAll(selectedFieldsMap);
        // 随机过期时间 次日凌晨1时-2时
        long expireSeconds = RedisUtil.tomorrowRandomExpireSeconds(1, 2, TimeUnit.HOURS);
        rMap.expire(expireSeconds, TimeUnit.SECONDS);
    }

    private CouponThemeCache doGetSelectedFieldsFromCouponThemeCache(Long couponThemeId, Set<CouponThemeCache.FIELDS> selectedFields) {
        CouponThemeCache couponThemeCache = null;
        // 从redis中获取券活动缓存
        Set<String> selectedStrFields = selectedFields.stream().map(CouponThemeCache.FIELDS::getName).collect(Collectors.toSet());
        RMap<String, Object> rMap = getCouponThemeRMap(couponThemeId);
        Map<String, Object> themeCacheMap = rMap.getAll(selectedStrFields);
        // 双重检查
        if (MapUtils.isEmpty(themeCacheMap)) {
            // 判定couponThemeId是否存在
            if (couponThemeIdIsMarkedAsNotExist(couponThemeId)) {
                return null;
            }
            String lockKeyName = RedisLockKeyConstant.SYNC_COUPON_THEME_DATABASE_TO_CACHE + couponThemeId;
            couponThemeCache = RedisUtil.executeLock(lockKeyName, 60, TimeUnit.SECONDS, () -> {
                Map<String, Object> cacheMap = rMap.getAll(selectedStrFields);
                // 双重检查
                if (MapUtils.isEmpty(cacheMap)) {
                    return rebuildCouponThemeCache(couponThemeId, rMap);
                }
                return BeanUtil.fillBeanWithMap(cacheMap, new CouponThemeCache(), false);
            });
        } else {
            couponThemeCache =  BeanUtil.fillBeanWithMap(themeCacheMap, new CouponThemeCache(), false);
        }

        return couponThemeCache;
    }


    /**
     * 将couponThemeId放入bloomFilter
     * @param couponThemeId 券活动主键id
     */
    private void markCouponThemeIdAsNotExist(Long couponThemeId) {
        /*couponThemeBloomFilter.tryInit(1000, 0.03);
        couponThemeBloomFilter.add(couponThemeId);
        // 获取明天0时偏移现在的毫秒数
        long tomorrowOffsetNowMs = DateUtil.betweenMs(DateTime.now(), DateUtil.beginOfDay(DateUtil.tomorrow()));
        couponThemeBloomFilter.expire(tomorrowOffsetNowMs, TimeUnit.MILLISECONDS);*/
        String cacheKey = RedisCacheKeyConstant.NON_EXIST_COUPON_THEME_ID + couponThemeId;
        RBucket<Integer> rBucket = redissonClient.getBucket(cacheKey);
        rBucket.set(1);
        rBucket.expire(3, TimeUnit.MINUTES);
    }

    private CouponThemeCache rebuildCouponThemeCache(Long couponThemeId, RMap<String, Object> rMap) {
        if (couponThemeIdIsMarkedAsNotExist(couponThemeId)) {
            return null;
        }
        log.info("couponThemeCache即将重建缓存: couponThemeId={}", couponThemeId);
        CouponTheme couponTheme = baseMapper.selectById(couponThemeId);
        if (Objects.isNull(couponTheme)) {
            // 标记此couponThemeId为不存在
            markCouponThemeIdAsNotExist(couponThemeId);
            log.error("数据库中不存在,couponThemeCache重建缓存失败, redis将couponThemeId标记为不存在: couponThemeId={}", couponThemeId);
            return null;
        }
        CouponThemeCache couponThemeCache = doAddCouponThemeCache(prepareCouponThemeCacheBean(couponTheme), rMap);
        log.info("couponThemeCache重建缓存ok: couponThemeId={}, couponThemeCache={}", couponThemeId, JSON.toJSONString(couponThemeCache));
        return couponThemeCache;
    }

    /**
     * 获取couponThemeCache的redis RMap连接对象
     * @param couponThemeId 券主题 主键
     * @return
     */
    private RMap<String, Object> getCouponThemeRMap(Long couponThemeId) {
        String keyName = RedisCacheKeyConstant.COUPON_THEME_DATABASE + couponThemeId;
        return redissonClient.getMap(keyName);
    }

    private CouponThemeCache doAddCouponThemeCache(CouponThemeCache couponThemeCache, RMap<String, Object> rMap) {
        Map<String, Object> map = BeanUtil.beanToMap(couponThemeCache);
        setSelectedFieldsToCouponThemeCache(map, rMap);
        return couponThemeCache;
    }

    protected CouponThemeCache prepareCouponThemeCacheBean(CouponTheme couponTheme) {
        CouponThemeCache couponThemeCache = new CouponThemeCache();
        BeanUtil.copyProperties(couponTheme, couponThemeCache);
        return couponThemeCache;
    }
}
